/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.basics.manager.impl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.springframework.util.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.genia.toolbox.basics.exception.technical.TechnicalFileNotFoundException;
import com.genia.toolbox.basics.exception.technical.TechnicalIOException;
import com.genia.toolbox.basics.exception.technical.XmlException;
import com.genia.toolbox.basics.manager.ExceptionManager;
import com.genia.toolbox.basics.manager.XmlManager;
import com.genia.toolbox.constants.client.Charset;

/**
 * Implementation of XmlManager, xml utilities.
 */
public class XmlManagerImpl
    implements XmlManager
{

  /**
   * the {@link DocumentBuilder} used by this manager. A
   * <code>ThreadLocal</code> is used because {@link DocumentBuilder} might
   * not be thread-safe.
   */
  private final transient ThreadLocal<DocumentBuilder> documentBuilder = new ThreadLocal<DocumentBuilder>();

  /**
   * the <code>DocumentBuilderFactory</code> used by this manager.
   */
  private DocumentBuilderFactory documentBuilderFactory = null;

  /**
   * the {@link SchemaFactory} to use.
   */
  private SchemaFactory schemaFactory = null;

  /**
   * the <code>TransformerFactory</code> used by this manager.
   */
  private TransformerFactory transformerFactory = null;

  /**
   * the {@link ExceptionManager} to use.
   */
  private ExceptionManager exceptionManager;



  /**
   * clone a Document into an other document.
   * 
   * @param document
   *          the input document.
   * @return it return the document cloned.
   * @throws XmlException
   *           technical exception during the parsing.
   */
  public Document clone(final Document document)
      throws XmlException

  {

    Node node = null;
    Node nodeCloned = null;

    // Open the xml file and get all node.
    final NodeList listNodeFromDomFile = document.getChildNodes();

    // create a new domFile
    Document newDomFile;

    newDomFile = newDocument();

    for (int i = 0; i < listNodeFromDomFile.getLength(); i++) {
      node = listNodeFromDomFile.item(i);

      nodeCloned = newDomFile.importNode(node, true);
      newDomFile.appendChild(nodeCloned);

    }
    return newDomFile;

  }



  /**
   * returns the {@link List} of all sub-{@link Element} of a {@link Element}.
   * 
   * @param element
   *          the {@link Element} to explore
   * @return the {@link List} of all sub-{@link Element} of a {@link Element}
   * @see com.genia.toolbox.basics.manager.XmlManager#getAllSubElements(org.w3c.dom.Element)
   */
  public List<Element> getAllSubElements(Element element)
  {
    List<Element> res = new ArrayList<Element>();
    res.add(element);
    for (Element subElement : getElements(element.getChildNodes())) {
      res.addAll(getAllSubElements(subElement));
    }
    return res;
  }



  /**
   * returns the {@link List} of all sub-{@link Node} of a {@link Node}.
   * 
   * @param node
   *          the {@link Node} to explore
   * @return the {@link List} of all sub-{@link Node} of a {@link Node}
   * @see com.genia.toolbox.basics.manager.XmlManager#getAllSubNodes(org.w3c.dom.Node)
   */
  public List<Node> getAllSubNodes(Node node)
  {
    List<Node> res = new ArrayList<Node>();
    res.add(node);
    NodeList nodeList = node.getChildNodes();
    for (int i = 0; i < nodeList.getLength(); i++) {
      res.addAll(getAllSubNodes(nodeList.item(i)));
    }
    return res;
  }



  /**
   * returns the value of an attribute of the {@link Element}. This method
   * return <code>null</code> if the attribute does not exist.
   * 
   * @param element
   *          the element to query
   * @param attributeName
   *          the name of the attribute to query
   * @return the value of the attribute of name attributeName, or
   *         <code>null</code> if no such attribute is defined
   * @see com.genia.toolbox.basics.manager.XmlManager#getAttribute(org.w3c.dom.Element,
   *      java.lang.String)
   */
  public String getAttribute(Element element, String attributeName)
  {
    if (element.hasAttribute(attributeName)) {
      return element.getAttribute(attributeName);
    }
    return null;
  }



  /**
   * returns the {@link List} of {@link Element} in a {@link NodeList}.
   * 
   * @param nodeList
   *          the {@link NodeList} to convert
   * @return the {@link List} of {@link Element} contained in the parameter
   *         {@link NodeList}
   * @see com.genia.toolbox.basics.manager.XmlManager#getElements(org.w3c.dom.NodeList)
   */
  public List<Element> getElements(NodeList nodeList)
  {
    return getElements(null, nodeList);
  }



  /**
   * returns the {@link List} of {@link Element} in a {@link NodeList}.
   * 
   * @param tagName
   *          the name of the tag of the elements to get, <code>null</code> if
   *          all elements must be retrieved
   * @param nodeList
   *          the {@link NodeList} to convert
   * @return the {@link List} of {@link Element} contained in the parameter
   *         {@link NodeList}
   */
  public List<Element> getElements(String tagName, NodeList nodeList)
  {
    List<Element> res = new ArrayList<Element>();
    for (int i = 0; i < nodeList.getLength(); i++) {
      Node node = nodeList.item(i);
      if (node.getNodeType() == Node.ELEMENT_NODE) {
        Element element = (Element) node;
        if (tagName == null || tagName.equals(element.getTagName())) {
          res.add(element);
        }
      }
    }
    return res;
  }



  /**
   * retrieve a {@link Schema} from a {@link File}.
   * 
   * @param file
   *          the {@link File} containing the schema.
   * @return a {@link Schema}
   * @throws XmlException
   *           if an error occured
   * @see com.genia.toolbox.basics.manager.XmlManager#getSchema(File[])
   */
  public Schema getSchema(File... file)
      throws XmlException
  {
    final Source[] sources = new Source[file.length];
    for (int i = 0; i < file.length; i++) {
      sources[i] = new StreamSource(file[i]);
    }
    return getSchema(sources);
  }



  /**
   * returns a {@link Schema} generated from an array of sources.
   * 
   * @param sources
   *          the array of sources
   * @return the generated {@link Schema}
   * @throws XmlException
   *           if an error occured
   */
  private Schema getSchema(Source[] sources)
      throws XmlException
  {
    try {
      return getSchemaFactory().newSchema(sources);
    }
    catch (SAXException e) {
      throw new XmlException(e);
    }
  }



  /**
   * retrieve a {@link Schema} from a {@link InputStream}.
   * 
   * @param inputStreams
   *          the {@link InputStream}s containing the schema.
   * @return a {@link Schema}
   * @throws XmlException
   *           if an error occured
   * @see com.genia.toolbox.basics.manager.XmlManager#getSchema(InputStream[])
   */
  public Schema getSchema(InputStream... inputStreams)
      throws XmlException
  {
    final Source[] sources = new Source[inputStreams.length];
    for (int i = 0; i < inputStreams.length; i++) {
      sources[i] = new StreamSource(inputStreams[i]);
    }
    return getSchema(sources);
  }



  /**
   * getter for the schemaFactory property.
   * 
   * @return the schemaFactory
   */
  public SchemaFactory getSchemaFactory()
  {
    if (schemaFactory == null) {
      schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    }
    return schemaFactory;
  }



  /**
   * returns a new {@link Document}.
   * 
   * @return a new document with version "1.0".
   * @throws XmlException
   *           technical exception during the parsing.
   */
  public Document newDocument()
      throws XmlException
  {
    // create a new document.
    return getDocumentBuilder().newDocument();
  }



  /**
   * get a document from an xml File.
   * 
   * @param file
   *          the path of the xml File.
   * @return a Document from the xml file parsed.
   * @throws XmlException
   *           technical exception during the parsing.
   * @throws TechnicalIOException
   *           when an I/O error occure
   */

  public Document parse(final File file)
      throws XmlException, TechnicalIOException
  {

    return parse(getInputStream(file));

  }



  /**
   * get a document from an inputStream.
   * 
   * @param inputStream
   *          stream from xmlFile.
   * @return a document from the inputStream.
   * @throws XmlException
   *           technical problem during the parsing.
   * @throws TechnicalIOException
   *           when an I/O error occure
   */

  public Document parse(final InputStream inputStream)
      throws XmlException, TechnicalIOException
  {
    try {

      return getDocumentBuilder().parse(inputStream);
    }
    catch (final SAXException e) {
      throw new XmlException(e);
    }
    catch (final IOException e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * print a document to an XmlFile.
   * 
   * @param document
   *          the input document.
   * @param file
   *          the output xml File.
   * @throws TechnicalFileNotFoundException
   *           exception thrown in case of wrong url of the file.
   * @throws XmlException
   *           technical problem.
   */
  public void print(final Document document, final File file)
      throws TechnicalFileNotFoundException, XmlException
  {

    try {

      final OutputStream outputStream = new FileOutputStream(file);

      print(document, outputStream);
    }
    catch (final FileNotFoundException e) {
      throw new TechnicalFileNotFoundException(e);
    }

  }



  /**
   * print from a Document to an OutputStream.
   * 
   * @param document
   *          the input document.
   * @param outputStream
   *          the output xml File.
   * @throws XmlException
   *           technical problem during the transformation of the document.
   */
  public void print(final Document document, final OutputStream outputStream)
      throws XmlException
  {

    // Create the Source from the domFile
    final Source domSource = new DOMSource(document);

    // Create the output StreamResult.
    final StreamResult result = new StreamResult(outputStream);

    try {
      final Transformer transformer = getTransformerFactory().newTransformer();
      // transformer.setOutputProperty(OutputKeys.INDENT, "yes");
      transformer.setOutputProperty(OutputKeys.ENCODING, Charset.UTF8);

      // transformation of the domSource.
      transformer.transform(domSource, result);
    }
    catch (final TransformerException e) {
      throw new XmlException(e);
    }

  }



  /**
   * setter for the documentBuilderFactory property.
   * 
   * @param documentBuilderFactory
   *          the documentBuilderFactory to set
   */
  public void setDocumentBuilderFactory(final DocumentBuilderFactory documentBuilderFactory)
  {
    this.documentBuilderFactory = documentBuilderFactory;
  }



  /**
   * setter for the transformerFactory property.
   * 
   * @param transformerFactory
   *          the transformerFactory to set
   */
  public void setTransformerFactory(final TransformerFactory transformerFactory)
  {
    this.transformerFactory = transformerFactory;
  }



  /**
   * strip all node that are only whitespace text.
   * 
   * @param node
   *          the root node to explore
   * @see com.genia.toolbox.basics.manager.XmlManager#stripWhiteSpaceNodes(org.w3c.dom.Node)
   */
  public void stripWhiteSpaceNodes(Node node)
  {
    node.normalize();
    for (Node subNode : getAllSubNodes(node)) {
      if (subNode.getNodeType() == Node.TEXT_NODE) {
        if (StringUtils.hasText(subNode.getNodeValue())) {
          subNode.setNodeValue(StringUtils.trimWhitespace(subNode.getNodeValue()));
        }
        else {
          subNode.getParentNode().removeChild(subNode);
        }
      }
    }
  }



  /**
   * the method transform an xml Document with an xsl Document into a document.
   * 
   * @param xslDom
   *          the xsl is of type Document.
   * @param xmlDom
   *          the xml is of type Document.
   * @return the xml transformed into a Document.
   * @throws XmlException
   *           technical problem during the transformation of the document.
   */
  public Document transform(final Document xslDom, final Document xmlDom)
      throws XmlException
  {

    final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

    print(xslDom, outputStream);

    return transform(new ByteArrayInputStream(outputStream.toByteArray()), xmlDom);

  }



  /**
   * the method transform an xml file with an xsl file into a document.
   * 
   * @param xslFile
   *          the xsl is of type File.
   * @param xmlFile
   *          the xml is of type File.
   * @return the xml transformed into a Document.
   * @throws XmlException
   *           technical problem during the transformation of the document.
   * @throws TechnicalFileNotFoundException
   *           when one of the file does not exist.
   */
  public Document transform(final File xslFile, final File xmlFile)
      throws XmlException, TechnicalFileNotFoundException
  {
    if (!xslFile.exists() || !xmlFile.exists()) {
      throw new TechnicalFileNotFoundException(new FileNotFoundException());
    }
    return transform(new StreamSource(xslFile), new StreamSource(xmlFile));
  }



  /**
   * the method transform an xml Document with an xsl InputStream into a
   * document.
   * 
   * @param xslStream
   *          the xsls of type InputStream.
   * @param xmlDom
   *          the xml is of type Document.
   * @return the xml transformed into a Document.
   * @throws XmlException
   *           technical problem during the transformation of the document.
   */
  public Document transform(final InputStream xslStream, final Document xmlDom)
      throws XmlException
  {
    return transform(new StreamSource(xslStream), new DOMSource(xmlDom));
  }



  /**
   * the method transform an xml InputStream with an xsl InputStream into a
   * document.
   * 
   * @param xslStream
   *          the xsl is of type InputStream.
   * @param xmlStream
   *          the xml is of type InputStream.
   * @return the xml transformed into a Document.
   * @throws XmlException
   *           technical problem during the transformation of the document.
   */
  public Document transform(final InputStream xslStream, final InputStream xmlStream)
      throws XmlException
  {
    return transform(new StreamSource(xslStream), new StreamSource(xmlStream));
  }



  /**
   * the method transform an xml Document with an xsl InputStream into a
   * document.
   * 
   * @param xslSource
   *          the {@link Source} representing the XSL file
   * @param xmlSource
   *          the {@link Source} representing the XML file
   * @return the {@link Document} representing the result of the transformation
   * @throws XmlException
   *           technical problem during the transformation of the document.
   */
  public Document transform(final Source xslSource, final Source xmlSource)
      throws XmlException
  {
    DOMResult outResult = new DOMResult();

    Transformer transformer = getTransformer(xslSource);
    try {
      transformer.transform(xmlSource, outResult);
    }
    catch (final TransformerException e) {
      throw new XmlException(e);
    }

    return (Document) outResult.getNode();
  }



  /**
   * the method transform an xml document given by its {@link URL} with an xsl
   * document given by its {@link URL} and returns a {@link Document}
   * representing the result.
   * 
   * @param xslUrl
   *          the {@link URL} representing the XSL file
   * @param xmlUrl
   *          the {@link URL} representing the XML file
   * @return the {@link Document} representing the result of the transformation
   * @throws XmlException
   *           technical problem during the transformation of the document.
   */
  public Document transform(final URL xslUrl, final URL xmlUrl)
      throws XmlException
  {
    return transform(new StreamSource(xslUrl.toString()), new StreamSource(xmlUrl.toString()));
  }



  /**
   * returns a new {@link DocumentBuilder}.
   * 
   * @return a new {@link DocumentBuilder}
   * @throws XmlException
   *           when the factory cannot produce a {@link DocumentBuilder}
   */
  private DocumentBuilder getDocumentBuilder()
      throws XmlException
  {
    try {
      DocumentBuilder result = documentBuilder.get();
      if (result == null) {
        result = getDocumentBuilderFactory().newDocumentBuilder();
        documentBuilder.set(result);
      }
      return result;
    }
    catch (final ParserConfigurationException e) {
      throw new XmlException(e);
    }
  }



  /**
   * getter for the documentBuilderFactory property.
   * 
   * @return the documentBuilderFactory
   */
  private DocumentBuilderFactory getDocumentBuilderFactory()
  {
    return documentBuilderFactory;
  }



  /**
   * returns an {@link InputStream} from a {@link File}.
   * 
   * @param file
   *          the {@link File}
   * @return an {@link InputStream} from a {@link File}
   * @throws TechnicalFileNotFoundException
   *           if the {@link File} does not exist
   */
  private InputStream getInputStream(final File file)
      throws TechnicalFileNotFoundException
  {
    try {
      return new FileInputStream(file);
    }
    catch (final FileNotFoundException e) {
      throw new TechnicalFileNotFoundException(e);
    }
  }



  /**
   * create an instance of transformer.
   * 
   * @param streamSource
   *          this parameter is generally a streamSource from xsl file.
   * @return an object transformer with xsl model.
   * @throws XmlException
   *           technical exception during the parsing.
   */
  private Transformer getTransformer(final Source streamSource)
      throws XmlException
  {
    try {
      return getTransformerFactory().newTransformer(streamSource);
    }
    catch (final TransformerException e) {
      throw new XmlException(e);
    }
  }



  /**
   * getter for the transformerFactory property.
   * 
   * @return the transformerFactory
   */
  private TransformerFactory getTransformerFactory()
  {
    return transformerFactory;
  }



  /**
   * getter for the exceptionManager property.
   * 
   * @return the exceptionManager
   */
  public ExceptionManager getExceptionManager()
  {
    return exceptionManager;
  }



  /**
   * setter for the exceptionManager property.
   * 
   * @param exceptionManager
   *          the exceptionManager to set
   */
  public void setExceptionManager(ExceptionManager exceptionManager)
  {
    this.exceptionManager = exceptionManager;
  }

}
